//
// Created by martin on 3/26/22.
//

#include "FtpDir.h"
#include "muduo/base/Logging.h"

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <assert.h>
#include <libgen.h>
#include <algorithm>
#include <stdio.h>
#include <dirent.h>
#include <vector>
#include <stddef.h>
#include <string>
#include <sstream>
#include <pwd.h>
#include <grp.h>
#include <sys/vfs.h>

namespace ftp {

const char *defaultRootDir()
{
    return FTP_ROOT_DIR_DEFAULT;
}

/**
 * 获得指定路径的文件的10bit权限位, 形如"-rwxrwxrwx"
 * @param filepath 文件路径
 * @param item [out] 要存放的权限位字符串
 */
int getPermission10bit(const std::string &filepath, std::string &item) {
    char perm[] = "----------"; /* "-rwxrwxrwx" */
    struct stat st;

    if (stat(filepath.c_str(), &st) < 0) {
        LOG_SYSERR << "getPermission10bit stat error";
        return -1;
    }

    /* file type */
    switch (st.st_mode & S_IFMT) {
        case S_IFREG: /* regular file */
            perm[0] = '-';
            break;
        case S_IFDIR:/* directory */
            perm[0] = 'd';
            break;
        case S_IFLNK: /* symbol link file(soft link) */
            perm[0] = 'l';
            break;
        case S_IFCHR: /* char device */
            perm[0] = 'c';
            break;
        case S_IFBLK: /* block device */
            perm[0] = 'b';
            break;
        default: /* unknown type, used as regular file */
            perm[0] = '-';
    }

    /* owner's permission */
    if (st.st_mode & S_IRUSR)
        perm[1] = 'r';
    if (st.st_mode & S_IWUSR)
        perm[2] = 'w';
    if (st.st_mode & S_IXUSR)
        perm[3] = 'x';

    /* group's permission */
    if (st.st_mode & S_IRGRP)
        perm[4] = 'r';
    if (st.st_mode & S_IWGRP)
        perm[5] = 'w';
    if (st.st_mode & S_IXGRP)
        perm[6] = 'x';

    /* other group permission */
    if (st.st_mode & S_IROTH)
        perm[7] = 'r';
    if (st.st_mode & S_IWOTH)
        perm[8] = 'w';
    if (st.st_mode & S_IXOTH)
        perm[9] = 'x';

    // copy perm back to item
    item += perm;
    return 0;
}

/**
 * 获取指定路径的文件的链接数
 */
int getFileLink(const std::string &filepath, std::string &item) {
    struct stat st;
    if (::stat(filepath.c_str(), &st) < 0) {
        LOG_SYSERR << "getFileLink error";
        return -1;
    }
    // 数字转字符串
    std::string res;
    std::stringstream ss;
    ss << st.st_nlink;
    ss >> res;

    item += res;
    return 0;
}

/**
 * 获取指定文件的拥有者, 并添加到字符串item
 */
int getFileOwner(const std::string &filepath, std::string &item) {
    struct stat st;
    struct passwd pwd;
    struct passwd *resPwd;
    long bufSize;
    int ret, res = 0;

    // get file uid
    if (::stat(filepath.c_str(), &st) << 0) {
        LOG_SYSERR << "getFileOwner stat error";
        return -1;
    }

    // get buffer max size
    if ((bufSize = sysconf(_SC_GETPW_R_SIZE_MAX)) == -1) {
        bufSize = 16384; // guess
    }

    char *buf = static_cast<char *>(malloc(static_cast<size_t>(bufSize)));
    if (buf == NULL) {
        LOG_SYSERR << "getFileOwner malloc error";
        return -1;
    }

    // get password file entry to get owner's name by uid
    int savedErrno = errno;
    errno = 0; // clear errno before call getpwuid_r()
    ret = getpwuid_r(st.st_uid, &pwd, buf, static_cast<size_t>(bufSize), &resPwd);
    if (ret != 0) { // error
        if (resPwd == NULL) { // ensure error
            LOG_SYSERR << "getFileOwner getpwuid_r error: file owner not found\n";
            res = -1;
        }
    }
    errno = savedErrno;

    if (res == 0) {
        item += pwd.pw_name;  // 将文件所属uid对应用户名，添加到item末尾
    }
    free(buf);
    return res;
}

int getFileGroup(const std::string &filepath, std::string &item) {
    struct stat st;
    long bufSize;
    int ret, res = 0;
    struct group grp, *resGrp;

    // get file gid
    if (::stat(filepath.c_str(), &st) < 0) { // error
        LOG_SYSERR << "getFileGroup stat error";
        return -1;
    }

    // get buffer max size
    if ((bufSize = sysconf(_SC_GETGR_R_SIZE_MAX)) == -1) {
        bufSize = 16384; // guess
    }
    char *buf = static_cast<char *>(::malloc(static_cast<size_t>(bufSize)));
    if (buf == NULL) {
        LOG_SYSERR << "getFileGroup malloc error";
        return -1;
    }

    // get group's name of gid
    ::getgrgid_r(st.st_gid, &grp, buf, static_cast<size_t>(bufSize), &resGrp);
    if (resGrp == NULL) {
        LOG_SYSERR << "getFileGroup getgrgid_r error";
        res = -1;
    }

    if (res == 0) {
        item += grp.gr_name;
    }
    free(buf);
    return res;
}

int getFileSize(const std::string &filepath, std::string &item) {
    struct stat st;
    // get file res
    if (::stat(filepath.c_str(), &st) < 0) { // error
        LOG_SYSERR << "getFileSize stat error";
        return -1;
    }

    std::stringstream ss;
    std::string res;
    ss << st.st_size;
    ss >> res;
    item += res;
    return 0;
}

/**
 * get last modify time (mtime) of file pointed by filepath
 * @details now time - mtime >= 0.5year, show brief time(year, month, day);
 * otherwise, show detail time(month, day, hour, minute)
 */
int getFileMTime(const std::string &filepath, std::string &item)
{
    struct stat st;
    // get file mtime
    if (::stat(filepath.c_str(), &st) < 0) { // error
        LOG_SYSERR << "getFileMTime stat error";
        return -1;
    }

    struct tm tmBuf;
    time_t nowTime = time(NULL);
    size_t n = 0;
    char buf[20];

    // convert file mtime to local time, storing in tmBuf
    if (gmtime_r(&st.st_mtime, &tmBuf) == NULL) { // call localtime_r() if use local time
        LOG_SYSERR << "getFileMTime gmtime_r error";
        return -1;
    }

    // within half a year, do not print year, but print hour and minute
    if (nowTime < st.st_mtime + (365 / 2) * 24 * 3600) {
        n = ::strftime(buf, sizeof(buf), "%b %d %R", &tmBuf); // i.g. Jan 19 21:24
    }
    else { // over half a year, print
        n = ::strftime(buf, sizeof(buf), "%b %d %Y", &tmBuf);
    }
    item += buf;
    return static_cast<int>(n);
}

/**
 * 删除尾部一个斜杠, 不包括最开始的那个元素
 */
std::string removeTailSlash(const std::string& pathname)
{
    if (pathname.empty()) return "";

    std::string str(pathname);
    if (pathname.size() > 1 && pathname.back() == '/') {
        str = pathname.substr(0, pathname.size() - 1);
    }
    return str;
}

} // namespace ftp

using namespace ftp;
using namespace muduo;

mode_t FtpDir::defaultMkdirMode = 0774;

FtpDir::FtpDir()
: rootDir_("/"),
  currentWorkingDir_("/"),
  physicalBaseDir_(defaultRootDir())
{
    assert(rootDir_ == "/");
    assert(physicalBaseDir_.front() == '/' && physicalBaseDir_.back() != '/');
    assert(currentWorkingDir_.front() == '/');
}

bool FtpDir::setCurrentDir(const std::string& newDir)
{
    if (newDir.empty()) return false;

    bool res = false;
    if (newDir == ".")
    {
        res = true;
    }
    else if (newDir == "..")
    {
        char dir[512];
        strncpy(dir, newDir.c_str(), sizeof(dir));
        currentWorkingDir_ = ::dirname(dir);
        res = true;
    }
    else if (newDir.front() == '/')
    {
        if (newDir.back() == '/')
        {
            currentWorkingDir_ = newDir.substr(0, newDir.size() - 1);
        }
        res = true;
    }

    return res;
}

bool FtpDir::isDir(const std::string& pathname) const
{
    bool res = false;
    string fullpath = absolutePath(pathname);
    struct stat st;
    int ret = ::stat(fullpath.c_str(), &st);
    if (0 == ret)
    {
        if (S_ISDIR(st.st_mode))
        {
            res = true;
        }
    }
    else if (ret < 0)
    {
        LOG_SYSERR << "stat error on file" << fullpath;
        res = false;
    }
    return res;
}

bool FtpDir::isRegularFile(const string &pathname) const
{
    bool res = false;
    string fullpath = absolutePath(pathname);
    struct stat st;
    int ret = ::stat(fullpath.c_str(), &st);
    if (0 == ret)
    {
        if (S_ISREG(st.st_mode))
        {
            res = true;
        }
    }
    else if (ret < 0)
    {
        LOG_SYSERR << "stat error on file" << fullpath;
    }
    return res;
}

bool FtpDir::exist(const std::string& pathname) const
{
    if (pathname.empty()) return false;
    string fullpath = absolutePath(pathname);

#if 0
    bool res = false;
    struct stat st;
    int ret = ::stat(fullpath.c_str(), &st);
    if (ret < 0)
    {
        if (errno == ENOENT)
        {
            res = false;
        }
        else
        {
            LOG_SYSERR << "stat error on file" << fullpath;
        }
    }
    else
    {
        res = true;
    }
    return res;
#else
    return 0 == ::access(fullpath.c_str(), F_OK);
#endif
}

bool FtpDir::chdir(const string& pathname)
{
    assert(rootDir_ == "/");
    bool res = false;
    if (pathname.empty() || !exist(pathname)) return false;

    if (pathname == ".")
    { return true; }
    else if (pathname == "..")
    {
        if (currentWorkingDir_ == rootDir_)
        {
            res = true;
        }
        else
        {
            // 找到父目录所对应斜杠位置
            size_t slashIndex = currentWorkingDir_.find_last_of('/');
            if (slashIndex == 0)
            { // 父目录是根目录
                currentWorkingDir_ = rootDir_;
                res = true;
            }
            else if (slashIndex > 0)
            { // 存在多个斜杠, 但不是首部(不是根目录)
                std::string parentDir = currentWorkingDir_.substr(0, slashIndex);
                std::swap(parentDir, currentWorkingDir_);
                assert(currentWorkingDir_[0] == '/');
                res = true;
            }
            else // string::npos
            { // 不存在, 出错, 正常情况下, 要求首部必须为'/'
                LOG_ERROR << "current working directory is unexpected: " << currentWorkingDir_;
                currentWorkingDir_ = rootDir_; // FIXME: 异常处理, 恢复到初始值
                res = false;
            }
        }
    }
    else if (pathname == rootDir_)
    {
        currentWorkingDir_ = rootDir_;
        res = true;
    }
    else if (pathname[0] == '/')
    { // 切换到一个绝对路径 (非物理路径), 只有该目录存在时,　才能切换
        if (isDir(pathname))
        {
            currentWorkingDir_ = pathname;
            res = true;
        }
    }
    else
    { // 不以 '/' 开头的路径, 看作相对路径
        if (isDir(pathname))
        {
            // 去掉尾部 '/'
            string tmpPath = removeTailSlash(pathname);
            if (!tmpPath.empty()) {
                if (currentWorkingDir_.back() == '/')
                    currentWorkingDir_ += tmpPath;
                else
                    currentWorkingDir_ += '/' + tmpPath;
            }
            res = true;
        }
    }
    return res;
}

string FtpDir::absolutePath(const string &pathname) const
{
    assert(rootDir_ == "/");
    assert(currentWorkingDir_[0] == '/');

    string fullPhysicalPath;
    if (pathname.empty()) return fullPhysicalPath;

    if (pathname == ".") { // 当前目录
        fullPhysicalPath = absolutePath();
    }
    else if (pathname == "..") { // 上一级目录
        if (currentWorkingDir_ == rootDir_) {
            fullPhysicalPath = absolutePath();
        }
        else {
            // 去掉末尾 "/"
            string tmpPath = currentWorkingDir_.back() == '/' ? currentWorkingDir_.substr(0, currentWorkingDir_.size() - 1) : currentWorkingDir_;
            size_t slash = tmpPath.find_last_of('/');
            if (slash == 0) {
                fullPhysicalPath = physicalBaseDir_ + rootDir_;
            }
            else {
                string parent = tmpPath.substr(0, slash);
                fullPhysicalPath = physicalBaseDir_ + parent;
            }
        }
    }
    else if (pathname[0] == '/') { // 绝对路径
        // 去掉重复的 间隔符('/')
        string tmp;
        tmp.reserve(pathname.size());
        tmp += pathname[0];
        for (size_t i = 1; i < pathname.size(); ++i) {
            if (pathname[i] != '/' || pathname[i] != pathname[i-1]) {
                tmp += pathname[i];
            }
        }

        // 组装成完整的物理路径, 确保末尾并非间隔符('/')
        if (tmp.size() > 1 && tmp[tmp.size() - 1] == '/')
            fullPhysicalPath = physicalBaseDir_ + tmp.substr(0, tmp.size() - 1);
        else
            fullPhysicalPath = physicalBaseDir_ + tmp;

        assert(fullPhysicalPath[0] == '/');
    }
    else { // 相对路径
        fullPhysicalPath = absolutePath() + '/' + pathname;
    }
    return fullPhysicalPath;
}

bool FtpDir::mkdir(const string &pathname)
{
    bool res = false;
    if (exist(pathname)) return false;

    string fullpath = absolutePath(pathname);
    int ret = ::mkdir(fullpath.c_str(), defaultMkdirMode);
    if (ret == 0) {
        res = true;
    }
    else {
        LOG_SYSERR << "mkdir error for " << fullpath;
        res = false;
    }
    return res;
}

bool FtpDir::removedir(const string &pathname)
{
    bool res = false;
    if (!exist(pathname)) return false;

    if (isDir(pathname)) {
        string fullpath = absolutePath(pathname);
        int ret;

        ret = ::rmdir(fullpath.c_str()); // 删除空目录
        if (ret == 0) {
            res = true;
        }
        else {
            LOG_SYSERR << "rmdir error for " << fullpath;
            res = false;
        }
    }
    return res;
}

bool FtpDir::remove(const string &pathname)
{
    bool res = false;
    if (!exist(pathname)) return false;

    if (isDir(pathname) || isRegularFile(pathname)) {
        string fullpath = absolutePath(pathname);
        int ret = ::remove(fullpath.c_str()); // delete a file or directory
        if (ret == 0) {
            res = true;
        }
        else {
            LOG_SYSERR << "remove error for " << fullpath;
            res = false;
        }
    }
    return res;
}

int FtpDir::rename(const string &oldpath, const string &newpath)
{
    if (oldpath == newpath) return 0; // 文件名相同

    string fullOldpath = absolutePath(oldpath);
    string fullNewpath = absolutePath(newpath);
    int ret = ::rename(fullOldpath.c_str(), fullNewpath.c_str());
    return ret;
}

std::vector<std::string> FtpDir::listFiles() const
{
    std::vector<std::string> files;
    DIR* dirp;
    struct dirent* dp;

    const string& fullDirPath = absolutePath(currentWorkingDir_);
    if ((dirp = ::opendir(fullDirPath.c_str())) == NULL) { // error
        LOG_SYSERR << "opendir error";
        return files;
    }
    
    struct stat st;
    struct dirent* entryp, *res_entryp;

    // readdir_r is deprecated since glibc 2.23 .
    // see: https://github.com/haskell/unix/pull/72
#if HAVE_READDIR_R
    long name_max = ::pathconf(fullDirPath.c_str(), _PC_NAME_MAX);
    if (name_max == -1)         /* Limit not defined, or error */
        name_max = 255;         /* Take a guess */
    size_t len = offsetof(struct dirent, d_name) + static_cast<unsigned long>(name_max) + 1;
    // malloc dirent object entry to store a item info
    entryp = static_cast<dirent *>(malloc(len));
#else

#endif

    // 遍历fullpath路径下的所有一级子文件
    while (true) {
        string fileitem;
        string fullFilePath;
        
#if HAVE_READDIR_R
        int ret;
        if ((ret = readdir_r(dirp, entryp, &res_entryp)) > 0) { // error
            LOG_SYSERR << "readdir_r error for current working directory " << fullDirPath;
            break;
        }
        if (res_entryp == NULL) // directory stream end
            break;
#else
        if ((entryp = readdir(dirp)) == NULL) { // error
            LOG_SYSERR << "readdir error for current working directory " << fullDirPath;
            break;
        }
#endif

        // ignore ".", ".."
        if (0 == strcmp(entryp->d_name, ".") || 0 == strcmp(entryp->d_name, ".."))
            continue;

        // get full file path
        fullFilePath = fullDirPath + "/" + entryp->d_name;

        // check if the file is writable
        if (::stat(fullDirPath.c_str(), &st) < 0) { // error
            LOG_SYSERR << "stat error for file " << fullFilePath;
            break;
        }

        // fill with 10bit permission
        getPermission10bit(fullFilePath, fileitem);
        fileitem += ' ';

        // fill with link counter
        getFileLink(fullFilePath, fileitem);
        fileitem += ' ';

        // fill with owner name
        getFileOwner(fullFilePath, fileitem);
        fileitem += ' ';

        // fill with group name
        getFileGroup(fullFilePath, fileitem);
        fileitem += ' ';

        // fill with file size
        getFileSize(fullFilePath, fileitem);
        fileitem += ' ';

        // fill with modify time
        getFileMTime(fullFilePath, fileitem);
        fileitem += ' ';

        // fill with file name
        fileitem += entryp->d_name;

        // add "\r\n" to the end of every fileitem
        fileitem += "\r\n";

        files.push_back(fileitem);
    }
    ::closedir(dirp);
    ::free(entryp);
    return files;
}

size_t FtpDir::fileSize(const string &pathname) const
{
    string fullpath = absolutePath(pathname);
    struct stat st;
    size_t numberBytes = 0;
    if (stat(fullpath.c_str(), &st) == 0)
    {
        numberBytes = static_cast<size_t>(st.st_size);
    }
    return numberBytes;
}

FtpDir::FileStatus FtpDir::saveOldPathnameForRename(const string &pathname)
{
    FtpDir::FileStatus res = FILESTATUS_OKAY;
    // TODO: 对文件加锁
    if (exist(pathname)) {
        oldPathname_ = pathname;
    }
    else
    { // file not exist
        res = FILESTATUS_NOTEXIST;
    }
    return res;
}

FtpDir::FileStatus FtpDir::renameFromSavedOldPathname(const string &newPathname)
{
    if (oldPathname_.empty()) return FILESTATUS_OLDFILENULL; // 没有设置要修改的文件名
    else if (!exist(oldPathname_)) return FILESTATUS_NOTEXIST; // 旧文件不存在

    if (exist(newPathname)) return FILESTATUS_FILENAMEUNVAILABLE; // 新命名文件已存在

    FtpDir::FileStatus res = FILESTATUS_OKAY;
    int ret = rename(oldPathname_, newPathname);
    if (ret < 0) {
        res = FILESTATUS_FILENAMEUNVAILABLE;
    }
    return res;
}

std::string FtpDir::absolutePath() const
{
    if (currentWorkingDir_.back() == '/') {
        return physicalBaseDir_ + currentWorkingDir_.substr(0, currentWorkingDir_.size() - 1);
    }
    else {
        return physicalBaseDir_ + currentWorkingDir_;
    }
}

bool FtpDir::writable(const string &pathname) const
{
    const string path = absolutePath(pathname);
    return 0 == ::access(path.c_str(), W_OK);
}

bool FtpDir::readable(const std::string& pathname) const
{
    const string path = absolutePath(pathname);
    return 0 == ::access(path.c_str(), R_OK);
}

string FtpDir::fullPath(const string &pathname) const
{
    string res;
    if (pathname.empty()) return pathname;

    if (pathname[0] == '/')  { // pathname is a full path
        res = pathname;
    } else { // pathname is a relative path
        if (currentWorkingDir_.back() == '/') {
            res = currentWorkingDir_ + pathname;
        }
        else {
            res = currentWorkingDir_ + '/' + pathname;
        }
    }
    return res;
}

unsigned long long FtpDir::getAvailSpace()
{
    struct statfs diskInfo;
    ::statfs(physicalBaseDir_.c_str(), &diskInfo);

    unsigned long long blockSize = static_cast<unsigned long long int>(diskInfo.f_bsize);
//    unsigned long long totalSize = diskInfo.f_blocks * blockSize;
    unsigned long long availableDisk = diskInfo.f_bavail * blockSize;
    return availableDisk;
}
